﻿using System;
using System.Collections.Generic;
using System.Threading;

namespace Pfz.Threading.Unsafe
{
	/// <summary>
	/// Inheriting this class you can program an enumerator in the most "natural" way, doing what it need to do
	/// and then returning and waiting until a new value is requested.
	/// This uses another thread to effectivelly do the enumeration and this is why it is considered "unsafe", as
	/// forgetting to dispose keeps a thread alive. So, to manipulate it assign it to a ThreadedIterator
	/// variable.
	/// </summary>
	public abstract class UnsafeThreadedEnumerator<T>:
		IDisposable
	{
		private readonly object _finishLock = new object();
		private bool _finished;
		private bool _disposed;
		private ManagedAutoResetEvent _nextFrameEvent = new ManagedAutoResetEvent();
		private ManagedAutoResetEvent _finishFrameEvent = new ManagedAutoResetEvent();
		internal T _current;

		private bool _useParallelLimit = true;

		/// <summary>
		/// Gets or sets a valud indicating if this threaded enumerator should be limited by the number of CPUs.
		/// If the limit is used, only one parallel iterator/enumerator is allowed per cpu.
		/// </summary>
		public bool UseParallelLimit
		{
			get
			{
				return _useParallelLimit;
			}
			set
			{
				_useParallelLimit = value;
			}
		}

		private bool _started;
		internal void _Start()
		{
			lock(_finishLock)
			{
				if (_started)
					return;

				_started = true;
			}

			try
			{
				UnlimitedThreadPool.Run(_Run);
			}
			catch
			{
				_DisposeEvents();
				throw;
			}
		}

		/// <summary>
		/// Asks the actual enumerator to finish, or releases all resources if it is already finished.
		/// </summary>
		public void Dispose()
		{
			lock(_finishLock)
			{
				if (_finished)
				{
					_DisposeEvents();
					return;
				}

				_disposed = true;
			}

			_nextFrameEvent.Set();
		}
		private void _DisposeEvents()
		{
			Disposer.Dispose(ref _nextFrameEvent);
			Disposer.Dispose(ref _finishFrameEvent);
		}

		/// <summary>
		/// Must be implemented to run the enumerator code.
		/// </summary>
		protected abstract void OnRun();

		private void _Run()
		{
			try
			{
				_nextFrameEvent.WaitOne();

				lock(_finishLock)
					if (_disposed)
						return;

				_ParallelWait();

				OnRun();
			}
			catch(IteratorDisposedException)
			{
			}
			finally
			{
				lock(_finishLock)
				{
					if (_disposed)
						_DisposeEvents();
					else
					{
						_finished = true;
						_finishFrameEvent.Set();
					}
				}

				_ParallelRelease();
			}
		}

		private bool _parallelSemaphoreGot;
		private void _ParallelWait()
		{
			if (UseParallelLimit)
			{
				UnsafeThreadedIterator._parallelSemaphore.Enter();
				_parallelSemaphoreGot = true;
			}
		}
		private void _ParallelRelease()
		{
			if (_parallelSemaphoreGot)
			{
				_parallelSemaphoreGot = false;
				UnsafeThreadedIterator._parallelSemaphore.Exit();
			}
		}

		/// <summary>
		/// Must be called to return value.
		/// If it returns false, the iterator was disposed, so it is better to return immediatelly.
		/// </summary>
		protected bool YieldReturnNoException(T result)
		{
			_ParallelRelease();

			lock(_finishLock)
			{
				if (_disposed)
					return false;

				_current = result;
			}

			var thread = Thread.CurrentThread;
			_finishFrameEvent.Set();
			thread.IsBackground = true;
			_nextFrameEvent.WaitOne();
			thread.IsBackground = false;

			lock(_finishLock)
				if (_disposed)
					return false;

			_ParallelWait();

			return true;
		}

		/// <summary>
		/// Similar to YieldReturnNoException, but throws an exception of type IteratorDisposedException
		/// if the enumerator was already disposed, this way you don't need to check for the bool result.
		/// </summary>
		protected void YieldReturn(T result)
		{
			if (!YieldReturnNoException(result))
				throw new IteratorDisposedException(GetType().FullName);
		}

		internal bool _BeginUpdate()
		{
			lock(_finishLock)
				if (_finished || _disposed)
					return false;

			_nextFrameEvent.Set();
			return true;
		}

		internal bool _EndUpdate(out T current)
		{
			_finishFrameEvent.WaitOne();

			bool result;
			lock(_finishLock)
			{
				result = !(_finished || _disposed);
				current = _current;
			}
			return result;
		}

		internal bool _Update(out T result)
		{
			if (!_BeginUpdate())
			{
				result = default(T);
				return false;
			}

			return _EndUpdate(out result);
		}
	}
}
